import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;


public class ConnectionHandler extends Thread{
	private static ConnectionHandler instance = new ConnectionHandler();
	private boolean serverStarted = true;
	private boolean gameHasStarted = false;
	private ArrayList<Client> connections = new ArrayList<Client>();
	private BufferedTcpSender bufferedTcpSender = new BufferedTcpSender();
	private Pinger pinger = new Pinger();

	private ConnectionHandler(){
	}
	public static ConnectionHandler getInstance() {
		if(instance == null) {
			instance = new ConnectionHandler();
		}
		return instance;
	}
	public void reset(){
		disconnectAllClients();
		connections.clear();
		gameHasStarted = false;
		System.out.println("Server reset was succesful.\n_______________________\n\n");
	}
	private void disconnectAllClients(){
		ArrayList<Client> clients = this.getActiveClients();
		for(Client client : clients){
			client.disconnect();
		}
	}

	public void startAcceptingConnections(){
		bufferedTcpSender.start();
		start();
	}
	public void stopAcceptingConnections(){
		bufferedTcpSender.stop();
		this.serverStarted = false;
	}

	@Override
	public void run(){
		System.out.println("Server has started.");
		handleConnections();
	}

	public void handleConnections(){
		ServerSocket welcomeSocket;
		try {
			welcomeSocket = new ServerSocket(Settings.port);

			while(serverStarted)
			{
				//Keep accepting new connections
				Socket connectionSocket;
				try {
					connectionSocket = welcomeSocket.accept();
					connectionSocket.setTcpNoDelay(true);
					if(getActiveClients().size() >= Settings.maximumAmountOfPlayers){
						 //SERVER IS FULL!
						declineConnection(connectionSocket, "The server is full!");		
					}
					else if(gameHasStarted){
						declineConnection(connectionSocket, "The game has already started!");	
					}
					else{
						Client previousConnection = findConnection(connectionSocket);
						if(previousConnection == null){ //If this is not a reconnect...
							//...add the connection / client.
							Client newConnection = new Client(connectionSocket);
							connections.add(newConnection);
						}
						else reconnectClient(previousConnection, connectionSocket);
					}
				} catch (IOException e) {
					e.printStackTrace(); //Could not accept connection
				}

			}

		} catch (IOException e1) {
			e1.printStackTrace(); //Server could not be started (ServerSocket not created?)
		}
	}
	
	public void declineConnection(Socket socket, String reason){
		System.out.println("X Connection declined: " + reason);
		Client tempClient = new Client(socket);
		tempClient.sendToClient("0_" + reason);
		tempClient = null;	
	}

	
	
	/**
	 * Checks if a reconnect is attempted, and executes the reconnection as well;
	 */
	private Client findConnection(Socket socket){
		for(int i = 0; i < this.connections.size(); i++){
			Client connection = connections.get(i);
			if(!connection.isConnected()){ //A disconnected client
				if(connection.getClientAddress().equals(socket.getInetAddress())){
					//IP's are the same!
					return connection;
				}
			}
		}
		return null;
	}

	/**
	 * Reconnect and enable this client.
	 * @param clientConnection ClientConnection that already existed and will be reconnected, keeping its old data.
	 * @param socket New Socket used for the connection.
	 */
	private void reconnectClient(Client client, Socket socket){
		client.reconnectClient(socket);
	}

	/**
	 * Sends data to all currently connected clients via TCP.
	 * @param data The data to send.
	 */
	public void sendToAllClients(String tcpdata){
		//bufferedTcpSender.addToBuffer(tcpdata);
		for (Client client : this.getActiveClients())
		{
			client.sendToClient(tcpdata);
		}
	}
	
	/**
	 * Sends data to the specified client via TCP.
	 * @param tcpdata The data to send.
	 * @param client The client to send the data to.
	 */
	public void sendToSingleClient(String tcpdata, Client client){
		//bufferedTcpSender.addToBufferSingleRecipient(tcpdata, client);
		client.sendToClient(tcpdata);
	}

	public int getAmountOfClients(){
		return this.connections.size();
	}

	/**
	 * Get all clients in the server, even clients that have been disconnected
	 * @return
	 */
	public ArrayList<Client> getAllClients(){
		return this.connections;
	}

	/**
	 * Get all clients that have an active connection with the server and have not disconnected
	 * @return
	 */
	public ArrayList<Client> getActiveClients(){
		ArrayList<Client> activeClients = new ArrayList<Client>();
		for(int i = 0; i < this.connections.size(); i++){
			if(this.connections.get(i).isConnected())
				activeClients.add(this.connections.get(i));
		}
		return activeClients;
	}
	
	public Client getClientById(int clientId){
		ArrayList<Client> activeClients = getActiveClients();
		for(int i = 0; i < activeClients.size(); i++){
			if(activeClients.get(i).getClientId() == clientId)
				return activeClients.get(i); //Found client by id
		}
		return null;
	}

	public void processNewClientData(Client newClient, String clientName, String clientWebId){
		String name = clientName;
		newClient.setName(name);
		newClient.setWebID(clientWebId);
		UserInterface.updateClients(getActiveClients());
		ArrayList<Client> activeClients = getActiveClients();
		String serverData = "2_";
		serverData += newClient.getClientId() + "_";
		serverData += Settings.respawnTime + "_";
		

		boolean firstClient = true;
		for(int i = 0; i < activeClients.size(); i++){
			Client activeClient = activeClients.get(i);

			if(activeClient.getClientId() != newClient.getClientId()){
				// Send tcp message to client
				sendToSingleClient("3_" + newClient.getClientId() + "_" + name, activeClient);
				if(!firstClient)
					serverData += "#";
				serverData += activeClient.getClientId() + ":" + activeClient.getName();
				firstClient = false;
			}
		}
		//Add respawn time.
		System.out.println("sent " + serverData);
		// Send tcp message to client
		sendToSingleClient(serverData, newClient);
		//Show notification in console
		String notificationString = "> " + newClient.getName() + " joined";
		if(newClient.getWebId() != "")
			notificationString += " (registered).";
		else notificationString += " (unregistered).";
		System.out.println(notificationString);
	}

	/**
	 * Changes the ready status of the client in the lobby
	 * @param changedClient
	 * @param newStatus
	 */
	public void clientLobbyReadyStatusChanged(Client changedClient, String newStatus){
		boolean newReadyStatus = changedClient.isReadyInLobby();
		if(newStatus.equals("0") || newStatus.equals("1")){
			if(newStatus.equals("0"))
				newReadyStatus = false;
			else if(newStatus.equals("1"))
				newReadyStatus = true;

			if(newReadyStatus != changedClient.isReadyInLobby()){
				changedClient.setReadyInLobbyStatus(newReadyStatus);
				sendToAllClients("5_" + changedClient.getClientId() + "_" + newStatus); //Type_id_status
				readyCheck();
			}
		}
		else System.out.println("Invalid ready status: " + newStatus);
	}

	public void clientDisconnected(Client disconnectedClient){
		if(this.getActiveClients().size() == 0){
			System.out.println("All clients have disconnected. Server will reset.");
			Game.getInstance().stopGame();
			reset();
		}
		else this.sendToAllClients("4_" + disconnectedClient.getClientId());
	}

	/**
	 * Executes a readycheck, starting the game if all Clients are ready to start.
	 */
	private void readyCheck(){
		if(areAllClientsReadyInLobby()){ //If everyone is ready
			//Load the level. When loading is done, the level is shared with the clients.
			Level.getInstance().loadLevel(LevelSelection.DEFAULTLEVEL);
			//this.sendToAllClients("6_LEVELDATA"); //Signal the clients that they should load the level that is sent.
			System.out.println("All players are ready. Sent load signal.");
			//Game.getInstance().startGame();
			//gameHasStarted = true;
		}	
	}
	
	public void clientHasFinishedLoading(Client finishedClient){
		finishedClient.setHasLoadedLevel(true);
		checkIfAllClientsLoaded();
	}

	private void checkIfAllClientsLoaded(){
		if(haveAllClientsFinishedLoading()){ //If all clients have finished loading the level
			Game.getInstance().startGame();
			gameHasStarted = true;
			this.sendToAllClients("7_" + Settings.matchDuration); //signal all clients the game has started.
			System.out.println("Players have finished loading. Sent start signal.");
		}
	}

	/**
	 * Checks whether all clients are ready in the lobby or not
	 * @return
	 */
	private boolean areAllClientsReadyInLobby(){
		if(this.getActiveClients().size() < Settings.minimumAmountOfPlayers) //Cant start with less than this amount of players
			return false;
		for(int i = 0; i < this.connections.size(); i++){
			if(this.connections.get(i).isConnected())
				if(!this.connections.get(i).isReadyInLobby()) //if client is not ready
					return false;
		}
		return true;
	}

	private boolean haveAllClientsFinishedLoading(){
		ArrayList<Client> clients = getActiveClients();
		for(int i = 0; i < clients.size(); i++){
			if(!clients.get(i).hasLoadedLevel()) //if client is not ready
				return false;
		}
		return true;
	}
	
	/**
	 * Gets the pinger, which calculates the ping for each client.
	 * @return The pinger.
	 */
	public Pinger getPinger() {
		return pinger;
	}

}
